iT邦幫忙

0

VScode 開發應用系統專案(5-1) - Spring Boot 排程作業(Schedule)

  • 分享至 

  • xImage
  •  

Spring Boot 排程作業(Schedule)控制

概述

  • Spring boot 提供了 Schedule Job 的功能,簡單的設定就可以依排程啟動程式,但應用系統可能分散式執行於多台主機,需要一個控制檔確認執行狀控。若需要完整的任務排程,可以加入一款功能強大的Quartz開源任務排程套件,因為多使用一個開源套件,多一分風險(弱點修復、版權問題),所以建議盡量以Spring Boot 提供的相依套件為主,這樣相依套件的版本,也會由spring-boot-starter-parent會幫忙管理,不必特別指定,當然若Spring Boot 提供的相依套件有弱點則可以於POM單獨排除,並指定特定版本使用 (相容性要測試)。

  • 準備提供基本的Schedule Job 功能,以方便後續增加任何排程,只需要比照案例,簡單增加一個Worker,其他都由設計好的架構控制與啟動。

** 遇到發文存檔失敗,所以分兩部分發文,這裡準備兩個檔案,分別控制檔案SysScheduleCtlEntity 以及排程執行紀錄檔案SysScheduleLogEntity。其他排程設定與執行相關,請參考(5-2) - Spring Boot 排程作業(Schedule) URL: https://ithelp.ithome.com.tw/articles/10398750

準備與檢核

  1. 建置Spring Boot專案後系統自動產生了 application.properties。
  1. 工具類程式已經準備好可以使用。
  1. Spring boot 多資料庫支援的配置。
  1. Spring Boot資料庫設計與存取 - 單一欄位 Key
  1. Spring Boot資料庫設計與存取 - 多欄位 Key

新增 ScheduleCtlEntity以及相關的存取程式。

主要是為了Schedule Job執行控制使用,依 dataType key值,

  • controller : 配合 runFlag,可以提值執行該 scheduleTaskName的 Job
  • masterServer : 配合多台主機分散執行時,唯有主機名稱masterServer的serverName 啟動時設定初始值。
  • taskLock : 配合多台主機分散執行時,每台主機都會檢查taskLock資料是否存在,若不存在表示正在執行中,若存在taskLock資料,則先Delete該筆資料,防止其他主機同時執行,結束時在產生新的taskLock資料,這裡採用Delete方式是因為遇到使用runFlag Yes/No 控制時,因為兩部主機的時間差太近,還來不及Update Flag 為 Yes, 兩部主機同時都讀到Yes,所以為了確保排程不會重複執行,以資料庫刪除來確保多台主機重複執行同一個Job,因為資料庫不可能重複Delete同一筆資料。

這裡只提供Entity資料以及測試程式,其他請參考以下 Spring Boot資料庫設計與存取文件

  • 多欄位 Key 參考URL: https://ithelp.ithome.com.tw/articles/10398727
  • 單一欄位 Key參考URL: :https://ithelp.ithome.com.tw/articles/10398629
    新增 Jpa reposiroty、mybatis等資料庫相關的存取設定,建議新的Entity執行一下測試程式,依經驗參照(Copy)類似程式調整時,常常會因疏忽大小寫或遺漏調整變數名稱而產生錯誤,也可以增強對系統除厝的經驗。

** Entity 程式

  1. ScheduleCtlEntity.java
package tw.lewishome.webapp.database.primary.entity;

import java.io.Serializable;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tw.lewishome.webapp.database.audit.EntityAudit;

/**
 * SysScheduleCtlEntity Table Entity
 *
 * @author lewis
 */
@Entity
// 資料庫的 Table 名稱 因為有需要以scheduleTaskName查詢與處理,所以這裡指定一個可重複的Index
@Table(name = "sysschedulectl", indexes = @Index(name = "idx_taskname", columnList = "scheduleTaskName", unique = false))
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class SysScheduleCtlEntity extends EntityAudit<String> {

    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new SysScheduleCtlEntity instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public SysScheduleCtlEntity() {
        // Constructor body (can be empty)
    }

    private static final long serialVersionUID = 1L;
    /** Primary Key */
    @EmbeddedId
    public DataKey dataKey;

    /** Server Name */
    @Column(name = "serverName", length = 128)
    public String serverName;

    /** Schedule Running Flag */
    @Column(name = "runFlag")
    public Boolean runFlag = true;

    /**
     * Entity Key
     *
     */
    @Embeddable
    @Data
    public static class DataKey implements Serializable {
        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new DataKey instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public DataKey() {
            // Constructor body (can be empty)
        }

        private static final long serialVersionUID = 1L;

        /** Schedule Task Name */
        @Column(name = "scheduleTaskName", length = 128)
        public String scheduleTaskName;

        /**
         * data Type for schedule controller
         * controller check runFlag for run or not
         * taskLock lock for task is running , if no tasklock record means task is not
         * running
         * masterServer schedule main server name , only main server can do schedule
         * need
         * initialize
         */

        @Column(name = "dataType", length = 64)
        public String dataType = ""; // controller or taskLock or masterServer

    }

    /** MyBatis TypeHandler for DataKey */
    public static class DataKeyHandler extends BaseTypeHandler<DataKey> {

        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new AsyncServiceWorkerSample instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public DataKeyHandler() {
            // Constructor body (can be empty)
        }

        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, DataKey parameter, JdbcType jdbcType)
                throws SQLException {
            try {
                ps.setString(1, parameter.getScheduleTaskName());
                ps.setString(2, parameter.getDataType());
                
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        @Override
        public DataKey getNullableResult(ResultSet rs, String columnName) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setScheduleTaskName(rs.getString("schedule_task_name"));
                dataKey.setDataType(rs.getString("data_type"));               
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setScheduleTaskName(rs.getString(1));
                dataKey.setDataType(rs.getString(2));
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (cs.wasNull() == false) {
                dataKey.setScheduleTaskName(cs.getString(1));
                dataKey.setDataType(cs.getString(2));
            }
            return dataKey;
        }
    }
}

** Repository 程式

基本Repository外,增加 deleteByDataType 的 Method.

  1. ScheduleCtlRepository.java
package tw.lewishome.webapp.database.primary.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import tw.lewishome.webapp.database.primary.entity.SysScheduleCtlEntity;

/**
 * SysScheduleCtl JPA Repository
 *
 * @author lewis
 * @version $Id: $Id
 */
@Transactional
@Repository
public interface SysScheduleCtlRepository extends JpaRepository<SysScheduleCtlEntity, SysScheduleCtlEntity.DataKey>,
                JpaSpecificationExecutor<SysScheduleCtlEntity> {

    @Modifying
    @Transactional
    @Query("DELETE FROM SysScheduleCtlEntity u WHERE u.dataKey.dataType = :parmDataType")
     void deleteByDataType(@Param("parmDataType") String dataType);

}

** Mapper 測試程式
3. SysScheduleCtlMapperTest.java

package tw.lewishome.webapp.database.primary.mybatis;
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import tw.lewishome.webapp.database.primary.entity.SysScheduleCtlEntity;

/**
 * SysScheduleCtlMapper Integration Tests
 * 
 * Tests for SysScheduleCtlMapper using MyBatis mapper with real database.
 * @BeforeEach to add test data and @AfterEach to clean up.
 *
 * @author lewis
 */
@SpringBootTest
@TestPropertySource(locations = "classpath:application.properties")
public class SysScheduleCtlMapperTest {

        @Autowired
        private SysScheduleCtlMapper sysScheduleCtlMapper;

        private SysScheduleCtlEntity testEntity;
        private SysScheduleCtlEntity testEntity2;

        /**
         * Setup test data before each test
         * Creates test schedule control entities and inserts them to database
         * Also cleans up any existing test data to ensure clean state
         */
        @BeforeEach
        void setUp() {
                // Clean up any existing test data
                cleanupTestSchedule("TEST_SCHEDULE_001", "TYPE_001");
                cleanupTestSchedule("TEST_SCHEDULE_002", "TYPE_002");

                // Create first test entity
                testEntity = new SysScheduleCtlEntity();
                SysScheduleCtlEntity.DataKey dataKey1 = new SysScheduleCtlEntity.DataKey();
                dataKey1.setScheduleTaskName("TEST_SCHEDULE_001");
                dataKey1.setDataType("TYPE_001");
                testEntity.setDataKey(dataKey1);
                testEntity.setServerName("SERVER_001");
                testEntity.setRunFlag(true);

                // Create second test entity
                testEntity2 = new SysScheduleCtlEntity();
                SysScheduleCtlEntity.DataKey dataKey2 = new SysScheduleCtlEntity.DataKey();
                dataKey2.setScheduleTaskName("TEST_SCHEDULE_002");
                dataKey2.setDataType("TYPE_002");
                testEntity2.setDataKey(dataKey2);
                testEntity2.setServerName("SERVER_002");
                testEntity2.setRunFlag(false);

                // Insert both test entities to database
                sysScheduleCtlMapper.insert(testEntity);
                sysScheduleCtlMapper.insert(testEntity2);
        }

        /**
         * Cleanup test data after each test
         * Removes all test entities from database
         */
        @AfterEach
        void tearDown() {
                cleanupTestSchedule("TEST_SCHEDULE_001", "TYPE_001");
                cleanupTestSchedule("TEST_SCHEDULE_002", "TYPE_002");
        }

        /**
         * Helper method to cleanup a test schedule by taskName and dataType
         * 
         * @param scheduleTaskName the schedule task name to cleanup
         * @param dataType the data type to cleanup
         */
        private void cleanupTestSchedule(String scheduleTaskName, String dataType) {
                sysScheduleCtlMapper.deleteByDataKey(scheduleTaskName, dataType);
        }

        /**
         * Test: Find all schedule control records
         * Verifies that findByAll returns all entities
         */
        @Test
        void testFindByAll() {
                // Query all records
                List<SysScheduleCtlEntity> found = sysScheduleCtlMapper.findByAll();

                // Verify entities were found
                assertNotNull(found);
                assertTrue(found.size() >= 2, "Should find at least 2 test schedules");

                // Verify test entities are in results
                boolean found1 = found.stream()
                                .anyMatch(e -> "TEST_SCHEDULE_001".equals(e.getDataKey().getScheduleTaskName()));
                assertTrue(found1, "Should find TEST_SCHEDULE_001");

                boolean found2 = found.stream()
                                .anyMatch(e -> "TEST_SCHEDULE_002".equals(e.getDataKey().getScheduleTaskName()));
                assertTrue(found2, "Should find TEST_SCHEDULE_002");
        }

        /**
         * Test: Find schedule control by data key
         * Verifies that findByDataKey returns correct entity
         */
        @Test
        void testFindByDataKey() {
                // Query by data key
                SysScheduleCtlEntity found = sysScheduleCtlMapper.findByDataKey("TEST_SCHEDULE_001", "TYPE_001");

                // Verify entity was found
                assertNotNull(found);
                assertEquals("TEST_SCHEDULE_001", found.getDataKey().getScheduleTaskName());
                assertEquals("TYPE_001", found.getDataKey().getDataType());
                assertEquals("SERVER_001", found.getServerName());
                assertTrue(found.getRunFlag());
        }

        /**
         * Test: Find schedule control by schedule task name
         * Verifies that findBySysScheduleTaskName returns matching entities
         */
        @Test
        void testFindBySysScheduleTaskName() {
                // Query by schedule task name
                List<SysScheduleCtlEntity> found = sysScheduleCtlMapper.findByScheduleTaskName("TEST_SCHEDULE_001");

                // Verify entity was found
                assertNotNull(found);
                assertTrue(found.size() >= 1, "Should find at least 1 schedule");

                // Verify first result matches
                assertEquals("TEST_SCHEDULE_001", found.get(0).getDataKey().getScheduleTaskName());
                assertEquals("SERVER_001", found.get(0).getServerName());
        }

        /**
         * Test: Insert schedule control record
         * Verifies that entity is inserted correctly
         */
        @Test
        void testInsertScheduleCtl() {
                // Create new entity
                SysScheduleCtlEntity newEntity = new SysScheduleCtlEntity();
                SysScheduleCtlEntity.DataKey dataKey = new SysScheduleCtlEntity.DataKey();
                dataKey.setScheduleTaskName("TEST_INSERT_SCHEDULE");
                dataKey.setDataType("TYPE_INSERT");
                newEntity.setDataKey(dataKey);
                newEntity.setServerName("SERVER_INSERT");
                newEntity.setRunFlag(true);

                // Insert entity
                int result = sysScheduleCtlMapper.insert(newEntity);

                // Verify insertion
                assertEquals(1, result, "Should insert 1 row");

                // Query to verify
                SysScheduleCtlEntity found = sysScheduleCtlMapper.findByDataKey("TEST_INSERT_SCHEDULE", "TYPE_INSERT");
                assertNotNull(found);
                assertEquals("SERVER_INSERT", found.getServerName());

                // Cleanup
                cleanupTestSchedule("TEST_INSERT_SCHEDULE", "TYPE_INSERT");
        }

        /**
         * Test: Delete schedule control record
         * Verifies that entity can be deleted
         */
        @Test
        void testDeleteByDataKey() {
                // Verify entity exists
                SysScheduleCtlEntity found = sysScheduleCtlMapper.findByDataKey("TEST_SCHEDULE_001", "TYPE_001");
                assertNotNull(found);

                // Delete entity
                int result = sysScheduleCtlMapper.deleteByDataKey("TEST_SCHEDULE_001", "TYPE_001");

                // Verify deletion
                assertEquals(1, result, "Should delete 1 row");

                // Query to verify deletion
                SysScheduleCtlEntity notFound = sysScheduleCtlMapper.findByDataKey("TEST_SCHEDULE_001", "TYPE_001");
                assertNull(notFound, "Entity should be deleted");
        }

        /**
         * Test: Verify all fields of saved entity
         * Ensures all entity fields are correctly stored and retrieved
         */
        @Test
        void testEntityFieldsArePersisted() {
                // Query entity
                SysScheduleCtlEntity found = sysScheduleCtlMapper.findByDataKey("TEST_SCHEDULE_002", "TYPE_002");

                // Verify all fields
                assertNotNull(found);
                assertEquals("TEST_SCHEDULE_002", found.getDataKey().getScheduleTaskName());
                assertEquals("TYPE_002", found.getDataKey().getDataType());
                assertEquals("SERVER_002", found.getServerName());
                assertFalse(found.getRunFlag());
        }

        /**
         * Test: Find schedule by different task names
         * Verifies that findBySysScheduleTaskName works for multiple records
         */
        @Test
        void testFindBySysScheduleTaskNameMultiple() {
                // Query for each task name
                List<SysScheduleCtlEntity> found1 = sysScheduleCtlMapper.findByScheduleTaskName("TEST_SCHEDULE_001");
                List<SysScheduleCtlEntity> found2 = sysScheduleCtlMapper.findByScheduleTaskName("TEST_SCHEDULE_002");

                // Verify results
                assertNotNull(found1);
                assertNotNull(found2);
                assertTrue(found1.size() >= 1);
                assertTrue(found2.size() >= 1);
                assertEquals("TEST_SCHEDULE_001", found1.get(0).getDataKey().getScheduleTaskName());
                assertEquals("TEST_SCHEDULE_002", found2.get(0).getDataKey().getScheduleTaskName());
        }

        /**
         * Test: Find non-existent schedule
         * Verifies that findByDataKey returns null for non-existent record
         */
        @Test
        void testFindNonExistentSchedule() {
                // Query for non-existent entity
                SysScheduleCtlEntity found = sysScheduleCtlMapper.findByDataKey("NON_EXISTENT", "TYPE_NONE");

                // Verify result is null
                assertNull(found, "Should not find non-existent entity");
        }
}

** Repository 測試程式
4. SysScheduleCtlRepositoryTest.java

package tw.lewishome.webapp.database.primary.repository;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import tw.lewishome.webapp.database.primary.entity.SysScheduleCtlEntity;

/**
 * SysScheduleCtlRepository Integration Tests
 * 
 * Tests for SysScheduleCtlRepository using real repository with
 * 
 * @BeforeEach to add test data and @AfterEach to clean up.
 *
 * @author lewis
 */


/**
 * SysScheduleCtlRepository Integration Tests
 * 
 * Tests for SysScheduleCtlRepository using real repository with
 * 
 * @BeforeEach to add test data and @AfterEach to clean up.
 *
 * @author lewis
 */
@SpringBootTest
@TestPropertySource(locations = "classpath:application.properties")
public class SysScheduleCtlRepositoryTest {

    @Autowired
    private SysScheduleCtlRepository sysScheduleCtlRepository;

    private SysScheduleCtlEntity testEntity;
    private SysScheduleCtlEntity testEntity2;

    /**
     * Setup test data before each test
     * Creates test schedule control entities and saves them to database
     * Also cleans up any existing test data to ensure clean state
     */
    @BeforeEach
    void setUp() {
        // Clean up any existing test data
        cleanupTestSchedule("TEST_SCHEDULE_001", "controller");
        cleanupTestSchedule("TEST_SCHEDULE_002", "taskLock");

        // Create first test entity
        testEntity = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey1 = new SysScheduleCtlEntity.DataKey();
        dataKey1.setScheduleTaskName("TEST_SCHEDULE_001");
        dataKey1.setDataType("controller");
        testEntity.setDataKey(dataKey1);
        testEntity.setServerName("SERVER_001");
        testEntity.setRunFlag(true);

        // Create second test entity
        testEntity2 = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey2 = new SysScheduleCtlEntity.DataKey();
        dataKey2.setScheduleTaskName("TEST_SCHEDULE_002");
        dataKey2.setDataType("taskLock");
        testEntity2.setDataKey(dataKey2);
        testEntity2.setServerName("SERVER_002");
        testEntity2.setRunFlag(false);

        // Save both test entities to database
        testEntity = sysScheduleCtlRepository.saveAndFlush(testEntity);
        testEntity2 = sysScheduleCtlRepository.saveAndFlush(testEntity2);
    }

    /**
     * Cleanup test data after each test
     * Removes all test entities from database
     */
    @AfterEach
    void tearDown() {
        cleanupTestSchedule("TEST_SCHEDULE_001", "controller");
        cleanupTestSchedule("TEST_SCHEDULE_002", "taskLock");
    }

    /**
     * Helper method to cleanup a test schedule by task name and data type
     * 
     * @param taskName the schedule task name to cleanup
     * @param dataType the data type to cleanup
     */
    private void cleanupTestSchedule(String taskName, String dataType) {
        SysScheduleCtlEntity.DataKey key = new SysScheduleCtlEntity.DataKey();
        key.setScheduleTaskName(taskName);
        key.setDataType(dataType);
        Optional<SysScheduleCtlEntity> entity = sysScheduleCtlRepository.findById(key);
        if (entity.isPresent()) {
            sysScheduleCtlRepository.delete(entity.get());
            sysScheduleCtlRepository.flush();
        }
    }

    /**
     * Test: Create new schedule control entity
     * Verifies that entity can be saved and returned with ID
     */
    @Test
    void testCreateScheduleEntity() {
        SysScheduleCtlEntity newEntity = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey = new SysScheduleCtlEntity.DataKey();
        dataKey.setScheduleTaskName("NEW_SCHEDULE");
        dataKey.setDataType("controller");
        newEntity.setDataKey(dataKey);
        newEntity.setServerName("NEW_SERVER");
        newEntity.setRunFlag(true);

        SysScheduleCtlEntity savedEntity = sysScheduleCtlRepository.saveAndFlush(newEntity);

        assertNotNull(savedEntity);
        assertEquals("NEW_SCHEDULE", savedEntity.getDataKey().getScheduleTaskName());
        assertEquals("controller", savedEntity.getDataKey().getDataType());
        assertEquals("NEW_SERVER", savedEntity.getServerName());
        assertEquals(true, savedEntity.getRunFlag());

        // Cleanup
        cleanupTestSchedule("NEW_SCHEDULE", "controller");
    }

    /**
     * Test: Read schedule entity by ID
     * Verifies that findById returns correct entity
     */
    @Test
    void testReadScheduleEntityById() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity.getDataKey());

        assertTrue(found.isPresent());
        assertEquals("TEST_SCHEDULE_001", found.get().getDataKey().getScheduleTaskName());
        assertEquals("controller", found.get().getDataKey().getDataType());
        assertEquals("SERVER_001", found.get().getServerName());
        assertEquals(true, found.get().getRunFlag());
    }

    /**
     * Test: Update schedule control entity
     * Verifies that entity updates are persisted correctly
     */
    @Test
    void testUpdateScheduleEntity() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(found.isPresent());

        SysScheduleCtlEntity entity = found.get();
        entity.setServerName("UPDATED_SERVER");
        entity.setRunFlag(false);

        sysScheduleCtlRepository.saveAndFlush(entity);

        Optional<SysScheduleCtlEntity> updated = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(updated.isPresent());
        assertEquals("UPDATED_SERVER", updated.get().getServerName());
        assertEquals(false, updated.get().getRunFlag());
    }

    /**
     * Test: Delete schedule control entity
     * Verifies that entity can be deleted and no longer found
     */
    @Test
    void testDeleteScheduleEntity() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(found.isPresent());

        sysScheduleCtlRepository.delete(testEntity);
        sysScheduleCtlRepository.flush();

        Optional<SysScheduleCtlEntity> notFound = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertFalse(notFound.isPresent());

        testEntity = null;
    }

    /**
     * Test: Find all schedule entities
     * Verifies that findAll returns all entities
     */
    @Test
    void testFindAllScheduleEntities() {
        List<SysScheduleCtlEntity> all = sysScheduleCtlRepository.findAll();

        assertNotNull(all);
        assertTrue(all.size() >= 2, "Should have at least 2 test schedules");

        long count = all.stream()
                .filter(e -> ("TEST_SCHEDULE_001".equals(e.getDataKey().getScheduleTaskName()) ||
                        "TEST_SCHEDULE_002".equals(e.getDataKey().getScheduleTaskName())))
                .count();
        assertEquals(2, count, "Should find exactly 2 test schedules");
    }

    /**
     * Test: Find by ID that does not exist
     * Verifies that findById returns empty Optional for non-existent entity
     */
    @Test
    void testFindScheduleEntityByIdNotFound() {
        SysScheduleCtlEntity.DataKey nonExistentKey = new SysScheduleCtlEntity.DataKey();
        nonExistentKey.setScheduleTaskName("NON_EXISTENT");
        nonExistentKey.setDataType("controller");

        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(nonExistentKey);

        assertFalse(found.isPresent());
    }

    /**
     * Test: Verify all fields are persisted correctly
     * Ensures all entity fields are correctly stored and retrieved
     */
    @Test
    void testEntityFieldsArePersisted() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity2.getDataKey());

        assertTrue(found.isPresent());
        SysScheduleCtlEntity entity = found.get();

        assertEquals("TEST_SCHEDULE_002", entity.getDataKey().getScheduleTaskName());
        assertEquals("taskLock", entity.getDataKey().getDataType());
        assertEquals("SERVER_002", entity.getServerName());
        assertEquals(false, entity.getRunFlag());
    }

    /**
     * Test: Toggle run flag
     * Verifies that run flag can be toggled and persisted
     */
    @Test
    void testToggleRunFlag() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(found.isPresent());

        SysScheduleCtlEntity entity = found.get();
        boolean originalFlag = entity.getRunFlag();

        entity.setRunFlag(!originalFlag);
        sysScheduleCtlRepository.saveAndFlush(entity);

        Optional<SysScheduleCtlEntity> updated = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(updated.isPresent());
        assertEquals(!originalFlag, updated.get().getRunFlag());
    }

    /**
     * Test: Count all schedules
     * Verifies that repository count method works correctly
     */
    @Test
    void testCountSchedules() {
        long count = sysScheduleCtlRepository.count();
        assertTrue(count >= 2, "Should have at least 2 schedules");
    }

    /**
     * Test: Save multiple entities
     * Verifies that multiple entities can be saved and retrieved
     */
    @Test
    void testSaveMultipleEntities() {
        SysScheduleCtlEntity entity3 = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey3 = new SysScheduleCtlEntity.DataKey();
        dataKey3.setScheduleTaskName("TEST_SCHEDULE_003");
        dataKey3.setDataType("masterServer");
        entity3.setDataKey(dataKey3);
        entity3.setServerName("SERVER_003");
        entity3.setRunFlag(true);

        SysScheduleCtlEntity entity4 = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey4 = new SysScheduleCtlEntity.DataKey();
        dataKey4.setScheduleTaskName("TEST_SCHEDULE_004");
        dataKey4.setDataType("controller");
        entity4.setDataKey(dataKey4);
        entity4.setServerName("SERVER_004");
        entity4.setRunFlag(false);

        sysScheduleCtlRepository.saveAndFlush(entity3);
        sysScheduleCtlRepository.saveAndFlush(entity4);

        List<SysScheduleCtlEntity> all = sysScheduleCtlRepository.findAll();
        assertTrue(all.size() >= 4, "Should have at least 4 schedules after saving");

        // Cleanup
        cleanupTestSchedule("TEST_SCHEDULE_003", "masterServer");
        cleanupTestSchedule("TEST_SCHEDULE_004", "controller");
    }

    /**
     * Test: Update server name
     * Verifies that server name field can be updated independently
     */
    @Test
    void testUpdateServerName() {
        Optional<SysScheduleCtlEntity> found = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(found.isPresent());

        SysScheduleCtlEntity entity = found.get();
        entity.setServerName("CHANGED_SERVER");
        sysScheduleCtlRepository.saveAndFlush(entity);

        Optional<SysScheduleCtlEntity> updated = sysScheduleCtlRepository.findById(testEntity.getDataKey());
        assertTrue(updated.isPresent());
        assertEquals("CHANGED_SERVER", updated.get().getServerName());
        assertEquals(true, updated.get().getRunFlag()); // Verify runFlag unchanged
    }

    /**
     * Test: deleteByDataType repository method
     * Verifies that all records with the given dataType are removed
     */
    @Test
    void testDeleteByDataType() {
        // Prepare entities with a unique dataType to avoid interfering with other tests
        String targetDataType = "deleteme";
        cleanupTestSchedule("DELETE_ME_001", targetDataType);
        cleanupTestSchedule("DELETE_ME_002", targetDataType);
        cleanupTestSchedule("KEEP_ME_001", "keepme");

        SysScheduleCtlEntity del1 = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dk1 = new SysScheduleCtlEntity.DataKey();
        dk1.setScheduleTaskName("DELETE_ME_001");
        dk1.setDataType(targetDataType);
        del1.setDataKey(dk1);
        del1.setServerName("S1");
        del1.setRunFlag(true);

        SysScheduleCtlEntity del2 = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dk2 = new SysScheduleCtlEntity.DataKey();
        dk2.setScheduleTaskName("DELETE_ME_002");
        dk2.setDataType(targetDataType);
        del2.setDataKey(dk2);
        del2.setServerName("S2");
        del2.setRunFlag(false);

        SysScheduleCtlEntity keep = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dk3 = new SysScheduleCtlEntity.DataKey();
        dk3.setScheduleTaskName("KEEP_ME_001");
        dk3.setDataType("keepme");
        keep.setDataKey(dk3);
        keep.setServerName("S3");
        keep.setRunFlag(true);

        sysScheduleCtlRepository.saveAndFlush(del1);
        sysScheduleCtlRepository.saveAndFlush(del2);
        sysScheduleCtlRepository.saveAndFlush(keep);

        // Sanity check: ensure records persisted
        List<SysScheduleCtlEntity> before = sysScheduleCtlRepository.findAll();
        long foundTargetBefore = before.stream()
                .filter(e -> targetDataType.equals(e.getDataKey().getDataType()))
                .count();
        assertTrue(foundTargetBefore >= 2, "Should have at least 2 records to delete for target dataType");

        // Perform deleteByDataType and flush
        sysScheduleCtlRepository.deleteByDataType(targetDataType);
        sysScheduleCtlRepository.flush();

        // Verify deletion
        List<SysScheduleCtlEntity> after = sysScheduleCtlRepository.findAll();
        long foundTargetAfter = after.stream()
                .filter(e -> targetDataType.equals(e.getDataKey().getDataType()))
                .count();
        assertEquals(0, foundTargetAfter, "All records with target dataType should be removed");

        // Verify other data types unaffected
        long keepCount = after.stream()
                .filter(e -> "keepme".equals(e.getDataKey().getDataType()))
                .count();
        assertTrue(keepCount >= 1, "Records with other dataTypes should remain");

        // Cleanup
        cleanupTestSchedule("DELETE_ME_001", targetDataType);
        cleanupTestSchedule("DELETE_ME_002", targetDataType);
        cleanupTestSchedule("KEEP_ME_001", "keepme");
    }
}

新增 ScheduleLogEntity以及相關的存取程式。

主要是為了紀錄Schedule Job 由哪一台何時開始執行以及結束執行(Audit的CreateDate與LastModifyDate)

package tw.lewishome.webapp.database.primary.entity;

import java.io.Serializable;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tw.lewishome.webapp.database.audit.EntityAudit;

/**
 * SysScheduleLog Table Entity
 *
 * @author lewis
 * @version $Id: $Id
 */
@Entity
@Table(name = "sysschedulelog") // 資料庫的 Table 名稱
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class SysScheduleLogEntity extends EntityAudit<String> {
        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new SysScheduleLogEntity instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public SysScheduleLogEntity() {
            // Constructor body (can be empty)
        }    
    private static final long serialVersionUID = 1L;
    /** Primary Key */
    @EmbeddedId
    public DataKey dataKey;

    /** Schedule Task Name */
    @Column(name = "scheduleTaskName", length = 128)
    public String scheduleTaskName;

    /** Server Name */
    @Column(name = "serverName", length = 128)
    public String serverName;

    /**
     * Entity Key
     *
     */
    @Embeddable
    @Data
    public static class DataKey implements Serializable {
        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new DataKey instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public DataKey() {
            // Constructor body (can be empty)
        }
        private static final long serialVersionUID = 1L;
        /** UUID */
        @Column(name = "uuid", length = 64)
        public String uuid = UUID.randomUUID().toString();
    }

     /**
     * MyBatis TypeHandler for DataKey
     */
    /**
     * MyBatis BaseTypeHandler} for converting between the application's
     * DataKey object and its JDBC representation.
     *
     */
    public static class DataKeyHandler extends BaseTypeHandler<DataKey> {

        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new AsyncServiceWorkerSample instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public DataKeyHandler() {
            // Constructor body (can be empty)
        }

        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, DataKey parameter, JdbcType jdbcType)
                throws SQLException {
            try {
                ps.setString(1, parameter.getUuid());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public DataKey getNullableResult(ResultSet rs, String columnName) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setUuid(rs.getString("uuid"));
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setUuid(rs.getString(1));
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (cs.wasNull() == false) {
                dataKey.setUuid(cs.getString(1));
            }
            return dataKey;
        }
    }

}

** Mapper 測試程式
2. SysSchedulelogMapperTest.java

package tw.lewishome.webapp.database.primary.mybatis;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import java.util.UUID;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import tw.lewishome.webapp.database.primary.entity.SysScheduleLogEntity;

/**
 * SysScheduleLogMapper Unit Tests
 * 
 * Tests for SysScheduleLogMapper using real mapper with database.
 *
 * @author lewis
 */
@SpringBootTest
public class SysScheduleLogMapperTest {

    @Autowired
    private SysScheduleLogMapper sysScheduleLogMapper;

    private SysScheduleLogEntity testEntity1;
    private SysScheduleLogEntity testEntity2;
    private String testUuid1;
    private String testUuid2;

    @BeforeEach
    void setUp() {
        // Generate test UUIDs
        testUuid1 = UUID.randomUUID().toString();
        testUuid2 = UUID.randomUUID().toString();

        // Create first test entity
        testEntity1 = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey dataKey1 = new SysScheduleLogEntity.DataKey();
        dataKey1.setUuid(testUuid1);
        testEntity1.setDataKey(dataKey1);
        testEntity1.setScheduleTaskName("TEST_TASK_001");
        testEntity1.setServerName("TEST_SERVER_001");

        // Create second test entity
        testEntity2 = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey dataKey2 = new SysScheduleLogEntity.DataKey();
        dataKey2.setUuid(testUuid2);
        testEntity2.setDataKey(dataKey2);
        testEntity2.setScheduleTaskName("TEST_TASK_002");
        testEntity2.setServerName("TEST_SERVER_002");

        // Save both test entities to database
        sysScheduleLogMapper.deleteById(testUuid1);
        sysScheduleLogMapper.insert(testEntity1);
        sysScheduleLogMapper.deleteById(testUuid2);
        sysScheduleLogMapper.insert(testEntity2);
    }

    /**
     * Cleanup test data after each test
     * Removes all test entities from database
     */
    @AfterEach
    void tearDown() {
        sysScheduleLogMapper.deleteById(testUuid1);
        sysScheduleLogMapper.deleteById(testUuid2);
    }

    /**
     * Test: Mapper is not null
     * Verifies that mapper is properly autowired
     */
    @Test
    void testMapperIsNotNull() {
        assertNotNull(sysScheduleLogMapper, "SysScheduleLogMapper should be autowired");
    }

    /**
     * Test: Find all returns list of entities
     * Verifies that findByAll returns all test entities
     */
    @Test
    void testFindByAllReturnsListOfEntities() {
        List<SysScheduleLogEntity> list = sysScheduleLogMapper.findByAll();
        assertNotNull(list);
        assertTrue(list.size() >= 2, "Should contain at least 2 test entities");
    }

    /**
     * Test: Find by UUID returns correct data
     * Verifies that findScheduleTaskName returns matching entity
     */
    @Test
    void testFindScheduleTaskNameReturnsCorrectData() {
        List<SysScheduleLogEntity> result = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");

        assertNotNull(result);
        assertTrue(result.size() >= 1, "Should find at least 1 result");

        boolean found = result.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) &&
                        "TEST_TASK_001".equals(e.getScheduleTaskName()));
        assertTrue(found, "Should find entity with UUID1 and task name TEST_TASK_001");
    }

    /**
     * Test: Find schedule task with no matches
     * Verifies that empty list is returned for non-matching query
     */
    @Test
    void testFindScheduleTaskNameReturnsEmptyList() {
        List<SysScheduleLogEntity> result = sysScheduleLogMapper.findScheduleTaskName("NON_EXISTENT_TASK");

        assertNotNull(result);
        boolean notFound = result.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) ||
                        testUuid2.equals(e.getDataKey().getUuid()));
        assertFalse(notFound, "Should not find test entities");
    }

    /**
     * Test: Find multiple invocations
     * Verifies that mapper can be called multiple times
     */
    @Test
    void testFindMultipleInvocations() {
        List<SysScheduleLogEntity> result1 = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");
        assertNotNull(result1);
        assertTrue(result1.size() >= 1);

        List<SysScheduleLogEntity> result2 = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_002");
        assertNotNull(result2);
        assertTrue(result2.size() >= 1);
    }

    /**
     * Test: Find by UUID returns all fields
     * Verifies that all fields are correctly retrieved from database
     */
    @Test
    void testFindScheduleTaskNameReturnsAllFields() {
        List<SysScheduleLogEntity> result = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");

        assertNotNull(result);
        assertTrue(result.size() >= 1);

        SysScheduleLogEntity found = result.stream()
                .filter(e -> testUuid1.equals(e.getDataKey().getUuid()))
                .findFirst()
                .orElse(null);

        assertNotNull(found);
        assertEquals(testUuid1, found.getDataKey().getUuid());
        assertEquals("TEST_TASK_001", found.getScheduleTaskName());
        assertEquals("TEST_SERVER_001", found.getServerName());
    }

    /**
     * Test: Insert and find entity
     * Verifies that inserted entity can be retrieved
     */
    @Test
    void testInsertAndFindEntity() {
        String newUuid = UUID.randomUUID().toString();
        SysScheduleLogEntity newEntity = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey dataKey = new SysScheduleLogEntity.DataKey();
        dataKey.setUuid(newUuid);
        newEntity.setDataKey(dataKey);
        newEntity.setScheduleTaskName("INSERT_TEST_TASK");
        newEntity.setServerName("INSERT_TEST_SERVER");

        // Insert
        int insertResult = sysScheduleLogMapper.insert(newEntity);
        assertEquals(1, insertResult, "Insert should return 1");

        // Find and verify
        List<SysScheduleLogEntity> found = sysScheduleLogMapper.findScheduleTaskName("INSERT_TEST_TASK");
        assertNotNull(found);
        assertTrue(found.size() >= 1);

        boolean hasInserted = found.stream()
                .anyMatch(e -> newUuid.equals(e.getDataKey().getUuid()) &&
                        "INSERT_TEST_TASK".equals(e.getScheduleTaskName()));
        assertTrue(hasInserted, "Should find inserted entity");

        // Cleanup
        sysScheduleLogMapper.deleteById(newUuid);
    }

    /**
     * Test: Update schedule log entity
     * Verifies that entity fields can be updated
     */
    @Test
    void testUpdateScheduleLogEntity() {
        // Verify entity exists before update
        List<SysScheduleLogEntity> beforeUpdate = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");
        assertNotNull(beforeUpdate);
        assertTrue(beforeUpdate.size() >= 1);

        // Update
        SysScheduleLogEntity entity = beforeUpdate.stream()
                .filter(e -> testUuid1.equals(e.getDataKey().getUuid()))
                .findFirst()
                .orElse(null);
        assertNotNull(entity);

        entity.setScheduleTaskName("UPDATED_TASK_001");
        entity.setServerName("UPDATED_SERVER_001");

        int updateResult = sysScheduleLogMapper.update(entity);
        assertEquals(1, updateResult, "Update should return 1");

        // Verify update
        List<SysScheduleLogEntity> afterUpdate = sysScheduleLogMapper.findScheduleTaskName("UPDATED_TASK_001");
        assertNotNull(afterUpdate);

        boolean hasUpdated = afterUpdate.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) &&
                        "UPDATED_TASK_001".equals(e.getScheduleTaskName()) &&
                        "UPDATED_SERVER_001".equals(e.getServerName()));
        assertTrue(hasUpdated, "Should find updated entity with new values");

        // Reset for cleanup
        entity.setScheduleTaskName("TEST_TASK_001");
        entity.setServerName("TEST_SERVER_001");
        sysScheduleLogMapper.update(entity);
    }

    /**
     * Test: Delete by UUID
     * Verifies that entity can be deleted and no longer found
     */
    @Test
    void testDeleteByUuid() {
        // Verify entity exists before delete
        List<SysScheduleLogEntity> beforeDelete = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");
        assertNotNull(beforeDelete);
        boolean foundBefore = beforeDelete.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()));
        assertTrue(foundBefore, "Entity should exist before delete");

        // Delete
        int deleteResult = sysScheduleLogMapper.deleteById(testUuid1);
        assertEquals(1, deleteResult, "Delete should return 1");

        // Verify entity is deleted
        List<SysScheduleLogEntity> afterDelete = sysScheduleLogMapper.findByAll();
        assertNotNull(afterDelete);
        boolean foundAfter = afterDelete.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()));
        assertFalse(foundAfter, "Entity should not be found after deletion");

        // Re-insert for tearDown cleanup
        testEntity1.setScheduleTaskName("TEST_TASK_001");
        testEntity1.setServerName("TEST_SERVER_001");
        sysScheduleLogMapper.insert(testEntity1);
    }

    /**
     * Test: Delete non-existent entity
     * Verifies that deleting non-existent entity returns 0
     */
    @Test
    void testDeleteNonExistentEntity() {
        String nonExistentUuid = UUID.randomUUID().toString();
        int deleteResult = sysScheduleLogMapper.deleteById(nonExistentUuid);
        assertEquals(0, deleteResult, "Delete non-existent entity should return 0");
    }

    /**
     * Test: Find second entity with correct data
     * Verifies second test entity data
     */
    @Test
    void testFindScheduleTaskNameReturnsCorrectDataForSecondEntity() {
        List<SysScheduleLogEntity> result = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_002");

        assertNotNull(result);
        assertTrue(result.size() >= 1);

        boolean found = result.stream()
                .anyMatch(e -> testUuid2.equals(e.getDataKey().getUuid()) &&
                        "TEST_TASK_002".equals(e.getScheduleTaskName()) &&
                        "TEST_SERVER_002".equals(e.getServerName()));
        assertTrue(found, "Should find second entity with correct data");
    }

    /**
     * Test: Update schedule task name only
     * Verifies that schedule task name can be updated independently
     */
    @Test
    void testUpdateScheduleTaskNameOnly() {
        // Get entity
        List<SysScheduleLogEntity> found = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_001");
        assertNotNull(found);
        assertTrue(found.size() >= 1);

        SysScheduleLogEntity entity = found.stream()
                .filter(e -> testUuid1.equals(e.getDataKey().getUuid()))
                .findFirst()
                .orElse(null);
        assertNotNull(entity);

        // Update only task name
        entity.setScheduleTaskName("PARTIAL_UPDATE_TASK");
        int updateResult = sysScheduleLogMapper.update(entity);
        assertEquals(1, updateResult, "Update should return 1");

        // Verify only task name changed
        List<SysScheduleLogEntity> updated = sysScheduleLogMapper.findScheduleTaskName("PARTIAL_UPDATE_TASK");
        assertNotNull(updated);

        boolean hasUpdated = updated.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) &&
                        "PARTIAL_UPDATE_TASK".equals(e.getScheduleTaskName()) &&
                        "TEST_SERVER_001".equals(e.getServerName())); // Original server name
        assertTrue(hasUpdated, "Only task name should change, server name should remain");

        // Reset for cleanup
        entity.setScheduleTaskName("TEST_TASK_001");
        sysScheduleLogMapper.update(entity);
    }

    /**
     * Test: Find by all with multiple invocations
     * Verifies that findByAll consistently returns all entities
     */
    @Test
    void testFindByAllMultipleInvocations() {
        List<SysScheduleLogEntity> all1 = sysScheduleLogMapper.findByAll();
        assertNotNull(all1);
        int initialSize = all1.size();

        List<SysScheduleLogEntity> all2 = sysScheduleLogMapper.findByAll();
        assertNotNull(all2);
        assertEquals(initialSize, all2.size(), "findByAll should return consistent results");

        boolean hasEntity1 = all2.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()));
        boolean hasEntity2 = all2.stream()
                .anyMatch(e -> testUuid2.equals(e.getDataKey().getUuid()));

        assertTrue(hasEntity1 && hasEntity2, "Should contain both test entities");
    }

    /**
     * Test: Update server name only
     * Verifies that server name can be updated independently
     */
    @Test
    void testUpdateServerNameOnly() {
        // Get entity
        List<SysScheduleLogEntity> found = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_002");
        assertNotNull(found);
        assertTrue(found.size() >= 1);

        SysScheduleLogEntity entity = found.stream()
                .filter(e -> testUuid2.equals(e.getDataKey().getUuid()))
                .findFirst()
                .orElse(null);
        assertNotNull(entity);

        // Update only server name
        entity.setServerName("UPDATED_SERVER_002");
        int updateResult = sysScheduleLogMapper.update(entity);
        assertEquals(1, updateResult, "Update should return 1");

        // Verify only server name changed
        List<SysScheduleLogEntity> updated = sysScheduleLogMapper.findScheduleTaskName("TEST_TASK_002");
        assertNotNull(updated);

        boolean hasUpdated = updated.stream()
                .anyMatch(e -> testUuid2.equals(e.getDataKey().getUuid()) &&
                        "TEST_TASK_002".equals(e.getScheduleTaskName()) && // Original task name
                        "UPDATED_SERVER_002".equals(e.getServerName()));
        assertTrue(hasUpdated, "Only server name should change, task name should remain");

        // Reset for cleanup
        entity.setServerName("TEST_SERVER_002");
        sysScheduleLogMapper.update(entity);
    }
}

** Repository 測試程式
3. SysSchedulelogRepositoryTest.java

 /**
     * Test: Find by ID that does not exist
     * Verifies that findById returns empty Optional for non-existent log
     */
    @Test
    void testFindByIdNotFound() {
        SysScheduleLogEntity.DataKey nonExistentKey = new SysScheduleLogEntity.DataKey();
        nonExistentKey.setUuid(UUID.randomUUID().toString());

        Optional<SysScheduleLogEntity> found = sysScheduleLogRepository.findById(nonExistentKey);

        assertFalse(found.isPresent());
    }

    /**
     * Test: Find schedule logs by schedule task name with no matches
     * Verifies that empty list is returned when no entities match
     */
    @Test
    void testFindAllScheduleLogByScheduleTaskName_NoMatch() {
        List<SysScheduleLogEntity> found = sysScheduleLogRepository
                .findAllScheduleLogByScheduleTaskName("NON_EXISTENT_TASK");

        assertNotNull(found);
        boolean hasTestLog = found.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) ||
                        testUuid2.equals(e.getDataKey().getUuid()));
        assertFalse(hasTestLog, "Should not find test schedule logs with non-matching task name");
    }

    /**
     * Test: Find schedule logs by task name with no matches (Native SQL)
     * Verifies that native SQL returns empty list when no entities match
     */
    @Test
    void testFindAllScheduleLogByScheduleTaskName2_NoMatch() {
        List<SysScheduleLogEntity> found = sysScheduleLogRepository
                .findAllScheduleLogByScheduleTaskName2("NON_EXISTENT_NATIVE_TASK");

        assertNotNull(found);
        boolean hasTestLog = found.stream()
                .anyMatch(e -> testUuid1.equals(e.getDataKey().getUuid()) ||
                        testUuid2.equals(e.getDataKey().getUuid()));
        assertFalse(hasTestLog, "Should not find test schedule logs with non-matching task name");
    }

    /**
     * Test: Save and retrieve schedule log with all fields
     * Verifies persistence and retrieval of complete entity
     */

    @Test
    void testSaveAndRetrieveCompleteScheduleLog() {
        String newUuid = UUID.randomUUID().toString();
        SysScheduleLogEntity newLog = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey newDataKey = new SysScheduleLogEntity.DataKey();
        newDataKey.setUuid(newUuid);
        newLog.setDataKey(newDataKey);
        newLog.setScheduleTaskName("COMPLETE_TEST_TASK");
        newLog.setServerName("COMPLETE_TEST_SERVER");

        SysScheduleLogEntity saved = sysScheduleLogRepository.saveAndFlush(newLog);

        assertNotNull(saved);
        Optional<SysScheduleLogEntity> retrieved = sysScheduleLogRepository.findById(saved.getDataKey());
        assertTrue(retrieved.isPresent());

        SysScheduleLogEntity entity = retrieved.get();
        assertEquals(newUuid, entity.getDataKey().getUuid());
        assertEquals("COMPLETE_TEST_TASK", entity.getScheduleTaskName());
        assertEquals("COMPLETE_TEST_SERVER", entity.getServerName());

        // Cleanup
        cleanupTestScheduleLog(newUuid);
    }

    /**
     * Test: Count total schedule logs
     * Verifies that repository count method works correctly
     */
    @Test
    void testCountScheduleLogs() {
        long count = sysScheduleLogRepository.count();
        assertTrue(count >= 2, "Should have at least 2 schedule logs");
    }

    /**
     * Test: Update schedule task name
     * Verifies that schedule task name field can be updated independently
     */

    @Test
    void testUpdateScheduleTaskName() {
        Optional<SysScheduleLogEntity> found = sysScheduleLogRepository.findById(testEntity.getDataKey());
        assertTrue(found.isPresent());

        SysScheduleLogEntity entity = found.get();
        entity.setScheduleTaskName("CHANGED_TASK_NAME");
        sysScheduleLogRepository.saveAndFlush(entity);

        Optional<SysScheduleLogEntity> updated = sysScheduleLogRepository.findById(testEntity.getDataKey());
        assertTrue(updated.isPresent());
        assertEquals("CHANGED_TASK_NAME", updated.get().getScheduleTaskName());
        assertEquals("SERVER_001", updated.get().getServerName()); // Verify serverName unchanged
    }

    /**
     * Test: Update server name
     * Verifies that server name field can be updated independently
     */

    @Test
    void testUpdateServerName() {
        Optional<SysScheduleLogEntity> found = sysScheduleLogRepository.findById(testEntity2.getDataKey());
        assertTrue(found.isPresent());

        SysScheduleLogEntity entity = found.get();
        entity.setServerName("CHANGED_SERVER");
        sysScheduleLogRepository.saveAndFlush(entity);

        Optional<SysScheduleLogEntity> updated = sysScheduleLogRepository.findById(testEntity2.getDataKey());
        assertTrue(updated.isPresent());
        assertEquals("CHANGED_SERVER", updated.get().getServerName());
        assertEquals("TEST_SCHEDULE_TASK_002", updated.get().getScheduleTaskName()); // Verify task name unchanged
    }

    /**
     * Test: Save multiple entities
     * Verifies that multiple entities can be saved and retrieved
     */
    @Test
    void testSaveMultipleEntities() {
        String uuid3 = UUID.randomUUID().toString();
        String uuid4 = UUID.randomUUID().toString();

        SysScheduleLogEntity entity3 = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey key3 = new SysScheduleLogEntity.DataKey();
        key3.setUuid(uuid3);
        entity3.setDataKey(key3);
        entity3.setScheduleTaskName("BATCH_TASK_001");
        entity3.setServerName("BATCH_SERVER_001");

        SysScheduleLogEntity entity4 = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey key4 = new SysScheduleLogEntity.DataKey();
        key4.setUuid(uuid4);
        entity4.setDataKey(key4);
        entity4.setScheduleTaskName("BATCH_TASK_002");
        entity4.setServerName("BATCH_SERVER_002");

        sysScheduleLogRepository.saveAndFlush(entity3);
        sysScheduleLogRepository.saveAndFlush(entity4);

        List<SysScheduleLogEntity> all = sysScheduleLogRepository.findAll();
        assertTrue(all.size() >= 4, "Should have at least 4 schedule logs after saving");

        // Cleanup
        cleanupTestScheduleLog(uuid3);
        cleanupTestScheduleLog(uuid4);
    }
}

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言